package uk.co.hs.domain.generic;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Transient;
import javax.persistence.Version;

import org.apache.commons.beanutils.BeanUtils;
import org.hibernate.Hibernate;
import org.hibernate.annotations.Type;
import org.hibernate.proxy.HibernateProxy;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;

import uk.co.hs.exceptions.NonNullableColumnException;

/**
 * UtilisoftGenericDomain.
 *
 * @param <IdType>
 */
@MappedSuperclass
public abstract class UtilisoftGenericDomain<IdType extends Number>
																										implements Serializable

{
  private static final long serialVersionUID = 1L;

  public static final String PK = "PK";
  public static final String VERSION = "VERSION";

  private IdType mPk;
  private Integer mVersion;
  private DateTime mLastUpdated;

  /**
   * Constructor.
   */
  protected UtilisoftGenericDomain()
  {
    setLastUpdated(new DateTime());
  }

  /**
   * Get the PK of this object.
   * @return The PK.
   */
  //@Id
  //@GeneratedValue(generator="incrementGenerator")
  //@GenericGenerator( name="incrementGenerator", strategy="increment")

  @Id
  @GeneratedValue(strategy=GenerationType.AUTO)
  @Column(name=PK, nullable=false)
  public IdType getPk()
  {
    return mPk;
  }

  /**
   * Set the PK of this object.
   * @param aPk The PK.
   */
  public void setPk(IdType aPk)
  {
    mPk = aPk;
  }

  /**
   * Hibernate Version of this object.
   * @return The Version.
   */
  @Column(name=VERSION, nullable=false)
  @Version
  public Integer getVersion()
  {
    return mVersion;
  }

  /**
   * Set the version of the object.
   * @param aVersion The Version.
   */
  public void setVersion(Integer aVersion)
  {
    mVersion = aVersion;
  }

  /**
   * Last Updated DateTime of this object.
   * @return Last Updated DateTime.
   */
  @Column(name="LAST_UPDATED", nullable=false)
  @Type(type="uk.co.hs.hibernate.JodaDateType")
  public DateTime getLastUpdated()
  {
    return mLastUpdated;
  }

  /**
   * Set the Last Updated DateTime of this object.
   * @param aLastUpdated The Last Updated DateTime of this object.
   */
  public void setLastUpdated(DateTime aLastUpdated)
  {
    mLastUpdated = aLastUpdated;
  }

  /**
   * Check getters for non nullable properties return a value
   */
  @Transient
  public void performNonNullableCheck(Boolean aWithTrace)
  {
  	Logger logger = LoggerFactory.getLogger(getClass());
  	Class<?> self = this.getClass();

  	StringBuilder errors = new StringBuilder();

  	//loop over declared methods for this class
  	for(Method method : Arrays.asList(ReflectionUtils.getAllDeclaredMethods(self)))
  	{
  		//for each method that is annotated with column
  		if(method.isAnnotationPresent(Column.class))
  		{
  			Column column = method.getAnnotation(Column.class); //grab the column annotation

  			//if column is non nullable
  			if(!column.nullable())
  			{
  				switch (method.getName()) {
  				//exclude checking the fields below
  				case "getPk":
					case "getVersion":
					case "getLastUpdated":
						break;

					default:
						try
						{
							Object val = method.invoke(this);
							if(val == null)
		  				{
								String errorMsg = String.format("Non Nullable is null: %s.%s()", self.getSimpleName(), method.getName());
								NonNullableColumnException ex = new NonNullableColumnException(errorMsg);
								logger.error(ex.toString());
								errors.append(ex);
		  				}
						}
						catch(IllegalAccessException | IllegalArgumentException| InvocationTargetException aE)
						{
							aE.printStackTrace();
						}
						break;
					}
  			}
  		}
  	}

  	if(errors.length() != 0 && aWithTrace)
  	{
  		throw new RuntimeException(errors.toString());
  	}
  }

  /**
   * Get a shallow clone of entity detached from the session. So
   * will create a duplicate row when made persistent
   *
   * @return Cloned object
   */
  @SuppressWarnings("unchecked")
  @Transient
  public Object getDetachedClone()
  {
  	try
  	{
	  	UtilisoftGenericDomain<Number> obj = this.getClass().newInstance();
	  	BeanUtils.copyProperties(obj, this);
	  	obj.setPk(null);
	  	obj.setVersion(null);
	  	obj.setLastUpdated(null);
			return obj;
		}
  	catch (IllegalAccessException | InstantiationException | InvocationTargetException aE)
		{
			aE.printStackTrace();
			return null;
		}
  }

  @SuppressWarnings("unchecked")
  @Transient
  public Object getDetachedCloneWithVersioning()
  {
    try
    {
      UtilisoftGenericDomain<Number> obj = this.getClass().newInstance();
      BeanUtils.copyProperties(obj, this);
      //obj.setPk(null);
      //obj.setVersion(null);
      obj.setLastUpdated(null);
      return obj;
    }
    catch (IllegalAccessException | InstantiationException | InvocationTargetException aE)
    {
      aE.printStackTrace();
      return null;
    }
  }

  /**
   * Utility method to fetch the actual object out of the lazy initialize
   * proxy
   *
   * @param aObject
   * @return
   */
  @SuppressWarnings("unchecked")
  public <T> T fetchLazyObject(Object aObject)
  {
  	if (!Hibernate.isInitialized(aObject))
  	{
  		Hibernate.initialize(aObject);
  		HibernateProxy proxy = (HibernateProxy) aObject;

  		if (proxy == null)
  		{
  			return null;
  		}
  		else
  		{
  			return (T)  proxy.getHibernateLazyInitializer().getImplementation();
  		}
  	}
  	return (T) aObject;
  }
}